# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate, nowdate, add_days

# 移除了循环导入的 JournalEntry

from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
    get_accounting_dimensions,
)
from erpnext.accounts.utils import get_fiscal_year

class BudgetError(frappe.ValidationError):
    pass

class DuplicateBudgetError(frappe.ValidationError):
    pass

class BudgetCN(Document):
    # begin: auto-generated types
    # This code is auto-generated. Do not modify anything in this block.
    from typing import TYPE_CHECKING
    if TYPE_CHECKING:
        from frappe.types import DF
        from erpnext.accounts.doctype.budget_account.budget_account import BudgetAccount

        accounts: DF.Table[BudgetAccount]
        action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
        action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
        action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
        action_if_annual_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
        action_if_annual_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
        action_if_annual_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
        amended_from: DF.Link | None
        applicable_on_booking_actual_expenses: DF.Check
        applicable_on_material_request: DF.Check
        applicable_on_purchase_order: DF.Check
        budget_against: DF.Literal["", "Cost Center", "Project"]
        company: DF.Link
        cost_center: DF.Link | None
        fiscal_year: DF.Link
        monthly_distribution: DF.Link | None
        naming_series: DF.Literal["BUDGET-.YYYY.-"]
        project: DF.Link | None
        # 新增字段
        enable_accrual: DF.Check
        accrual_day: DF.Int
        accrual_journal_template: DF.Link | None
    # end: auto-generated types

    def validate(self):
        if not self.get(frappe.scrub(self.budget_against)):
            frappe.throw(_("{0} is mandatory").format(self.budget_against))
        self.validate_duplicate()
        self.validate_accounts()
        self.set_null_value()
        self.validate_applicable_for()
        self.validate_accrual_settings()  # 新增校验

    def validate_duplicate(self):
        budget_against_field = frappe.scrub(self.budget_against)
        budget_against = self.get(budget_against_field)
        accounts = [d.account for d in self.accounts] or []
        
        # 关键修改：为 budget_against_field 加上表别名 b.，明确指向主表
        existing_budget = frappe.db.sql(
            """
            select
            b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
            where
            ba.parent = b.name and b.docstatus < 2 and b.company = %s and b.{0}=%s and
            b.fiscal_year=%s and b.name != %s and ba.account in ({1})
            """.format(
                budget_against_field,  # 此处已添加 b. 前缀，指向主表的字段
                ",".join(["%s"] * len(accounts))
            ),
            (self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)),
            as_dict=1,
        )
        
        for d in existing_budget:
            frappe.throw(
                _(
                    "Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}"
                ).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
                DuplicateBudgetError,
            )

    def validate_accounts(self):
        account_list = []
        for d in self.get("accounts"):
            if d.account:
                account_details = frappe.get_cached_value(
                    "Account", d.account, ["is_group", "company", "report_type"], as_dict=1
                )
                if account_details.is_group:
                    frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
                elif account_details.company != self.company:
                    frappe.throw(
                        _("Account {0} does not belongs to company {1}").format(d.account, self.company)
                    )
                elif account_details.report_type != "Profit and Loss":
                    frappe.throw(
                        _(
                            "Budget cannot be assigned against {0}, as it's not an Income or Expense account"
                        ).format(d.account)
                    )
                if d.account in account_list:
                    frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
                else:
                    account_list.append(d.account)

    def set_null_value(self):
        if self.budget_against == "Cost Center":
            self.project = None
        else:
            self.cost_center = None

    def validate_applicable_for(self):
        if self.applicable_on_material_request and not (
            self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses
        ):
            frappe.throw(
                _("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses")
            )
        elif self.applicable_on_purchase_order and not (self.applicable_on_booking_actual_expenses):
            frappe.throw(_("Please enable Applicable on Booking Actual Expenses"))
        elif not (
            self.applicable_on_material_request
            or self.applicable_on_purchase_order
            or self.applicable_on_booking_actual_expenses
        ):
            self.applicable_on_booking_actual_expenses = 1

    # 新增：预提设置校验
    def validate_accrual_settings(self):
        if self.enable_accrual:
            if not self.accrual_day or self.accrual_day < 1 or self.accrual_day > 28:
                frappe.throw(_("Accrual Day must be between 1 and 28"))
            if not self.accrual_journal_template:
                frappe.throw(_("Accrual Journal Template is mandatory when Accrual is enabled"))
            # 校验模板有效性
            template = frappe.get_cached_value("Journal Entry Template", self.accrual_journal_template, "company")
            if template != self.company:
                frappe.throw(_("Journal Template must belong to the same company as the budget"))

    # 新增：生成预提记账草稿
    def generate_accrual_journal(self):
        """每月自动生成预提记账草稿"""
        if not self.enable_accrual:
            return
        
        # 延迟导入以避免循环导入
        from erpnext.accounts.doctype.journal_entry.journal_entry import JournalEntry
        from frappe.utils import get_month, get_year, nowdate

        current_date = nowdate()
        fiscal_year = get_fiscal_year(current_date, company=self.company)[0]
        if fiscal_year != self.fiscal_year:
            return  # 不在当前预算的会计年度内
        
        # 计算当月预算金额
        for acc in self.accounts:
            if not acc.budget_amount:
                continue
            
            # 获取月度分配比例
            monthly_amount = acc.budget_amount
            if self.monthly_distribution:
                dist = frappe.get_doc("Monthly Distribution", self.monthly_distribution)
                current_month = get_month(current_date)
                for d in dist.distributions:
                    if d.month == current_month:
                        monthly_amount = acc.budget_amount * (d.percentage / 100)
                        break

            # 创建记账草稿
            je = frappe.new_doc("Journal Entry")
            je.voucher_type = "Journal Entry"
            je.company = self.company
            je.posting_date = current_date
            je.naming_series = "ACC-JV-.YYYY.-"
            je.user_remark = f"Accrual for {self.name} - {acc.account} ({current_month} {get_year(current_date)})"
            
            # 从模板加载分录
            template = frappe.get_doc("Journal Entry Template", self.accrual_journal_template)
            for row in template.journal_entries:
                je.append("accounts", {
                    "account": row.account if row.account else acc.account,
                    "cost_center": self.cost_center if self.budget_against == "Cost Center" else None,
                    "project": self.project if self.budget_against == "Project" else None,
                    "debit_in_account_currency": row.debit_in_account_currency if row.is_debit else 0,
                    "credit_in_account_currency": monthly_amount if row.is_credit else 0,
                    "budget_item": acc.budget_item,
                    "remarks": row.remarks
                })
            
            je.save(ignore_permissions=True)
            frappe.msgprint(_("Generated accrual journal draft: {0}").format(je.name))

# 新增：定时任务触发方法
def generate_monthly_accrual_journals():
    """每月执行一次，生成所有启用预提的预算的记账草稿"""
    today = getdate(nowdate())
    budgets = frappe.get_list("Budget", 
        filters={"enable_accrual": 1, "docstatus": 1},
        fields=["name", "accrual_day"]
    )
    for budget in budgets:
        if today.day == budget.accrual_day:
            try:
                doc = frappe.get_doc("Budget", budget.name)
                doc.generate_accrual_journal()
            except Exception as e:
                frappe.log_error(f"Failed to generate accrual for {budget.name}: {str(e)}")

def validate_expense_against_budget(args, expense_amount=0):
    args = frappe._dict(args)
    if not frappe.get_all("Budget", limit=1):
        return
    if args.get("company") and not args.fiscal_year:
        args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
    frappe.flags.exception_approver_role = frappe.get_cached_value(
        "Company", args.get("company"), "exception_budget_approver_role"
    )
    if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}):
        return
    if not args.account:
        args.account = args.get("expense_account")
    if not (args.get("account") and args.get("cost_center")) and args.item_code:
        args.cost_center, args.account = get_item_details(args)
    if not args.account:
        return
    default_dimensions = [
        {
            "fieldname": "project",
            "document_type": "Project",
        },
        {
            "fieldname": "cost_center",
            "document_type": "Cost Center",
        },
    ]
    for dimension in default_dimensions + get_accounting_dimensions(as_list=False):
        budget_against = dimension.get("fieldname")
        if (
            args.get(budget_against)
            and args.account
            and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
        ):
            doctype = dimension.get("document_type")
            if frappe.get_cached_value("DocType", doctype, "is_tree"):
                lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"])
                condition = f"""and exists(select name from `tab{doctype}`
                    where lft<={lft} and rgt>={rgt} and name=b.{budget_against})"""
                args.is_tree = True
            else:
                condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}"
                args.is_tree = False
            args.budget_against_field = budget_against
            args.budget_against_doctype = doctype
            budget_records = frappe.db.sql(
                f"""
                select
                b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution,
                ifnull(b.applicable_on_material_request, 0) as for_material_request,
                ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
                ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
                b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded,
                b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr,
                b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po
                from
                `tabBudget` b, `tabBudget Account` ba
                where
                b.name=ba.parent and b.fiscal_year=%s
                and ba.account=%s and b.docstatus=1
                {condition}
                """,
                (args.fiscal_year, args.account),
                as_dict=True,
            )
            if budget_records:
                validate_budget_records(args, budget_records, expense_amount)

def validate_budget_records(args, budget_records, expense_amount):
    for budget in budget_records:
        if flt(budget.budget_amount):
            yearly_action, monthly_action = get_actions(args, budget)
            args["for_material_request"] = budget.for_material_request
            args["for_purchase_order"] = budget.for_purchase_order
            if yearly_action in ("Stop", "Warn"):
                compare_expense_with_budget(
                    args,
                    flt(budget.budget_amount),
                    _("Annual"),
                    yearly_action,
                    budget.budget_against,
                    expense_amount,
                )
            if monthly_action in ["Stop", "Warn"]:
                budget_amount = get_accumulated_monthly_budget(
                    budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
                )
                args["month_end_date"] = get_last_day(args.posting_date)
                compare_expense_with_budget(
                    args,
                    budget_amount,
                    _("Accumulated Monthly"),
                    monthly_action,
                    budget.budget_against,
                    expense_amount,
                )

def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
    args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
    if not amount:
        args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
    if args.get("doctype") == "Material Request" and args.for_material_request:
        amount = args.requested_amount + args.ordered_amount
    elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
        amount = args.ordered_amount
    total_expense = args.actual_expense + amount
    if total_expense > budget_amount:
        if args.actual_expense > budget_amount:
            error_tense = _("is already")
            diff = args.actual_expense - budget_amount
        else:
            error_tense = _("will be")
            diff = total_expense - budget_amount
        currency = frappe.get_cached_value("Company", args.company, "default_currency")
        msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format(
            _(action_for),
            frappe.bold(args.account),
            frappe.unscrub(args.budget_against_field),
            frappe.bold(budget_against),
            frappe.bold(fmt_money(budget_amount, currency=currency)),
            error_tense,
            frappe.bold(fmt_money(diff, currency=currency)),
        )
        msg += get_expense_breakup(args, currency, budget_against)

# 补充缺失的函数定义
def get_actions(args, budget):
    # 根据文档类型返回对应的动作配置
    if args.doctype == "Material Request":
        return budget.action_if_annual_budget_exceeded_on_mr, budget.action_if_accumulated_monthly_budget_exceeded_on_mr
    elif args.doctype == "Purchase Order":
        return budget.action_if_annual_budget_exceeded_on_po, budget.action_if_accumulated_monthly_budget_exceeded_on_po
    else:  # 实际费用
        return budget.action_if_annual_budget_exceeded, budget.action_if_accumulated_monthly_budget_exceeded

def get_item_details(args):
    # 获取物料对应的成本中心和账户
    item_details = frappe.get_cached_value("Item", args.item_code, ["expense_account", "default_cost_center"])
    return item_details[1], item_details[0]

def get_actual_expense(args):
    # 获取实际费用金额
    return 0  # 实际实现需要根据业务逻辑查询

def get_requested_amount(args):
    # 获取申请金额
    return 0  # 实际实现需要根据业务逻辑查询

def get_ordered_amount(args):
    # 获取订单金额
    return 0  # 实际实现需要根据业务逻辑查询

def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, budget_amount):
    # 计算累计月度预算
    return budget_amount  # 实际实现需要根据月度分配计算

def get_expense_breakup(args, currency, budget_against):
    # 获取费用明细
    return ""  # 实际实现需要返回费用明细信息